﻿using System;
using System.Web;
using System.Net;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Globalization;
using System.Xml.Serialization;
using System.Collections.Generic;
using System.Reflection;

namespace GolfSigma.Inventory
{
    public class InventoryClient
    {
        private const int _MaxBatchSize = 100; 
        private const string _HeaderPrefix = "x-gsws-";
        private string _AccessKeyId = null;
        private string _SecretAccessKey = null;
        public InventoryConfig Config = null;

        #region Public API

        public Model.GetTeeTimeResponse GetTeeTime(Model.GetTeeTimeRequest request)
        {
            // Who passes in an empty guid anyways?
            if (request.TeeTimeId < 1)
                throw new NullReferenceException("Invalid TeeTimeId provided. Unsigned Long/Int64 expected.");

            return Invoke<Model.GetTeeTimeResponse>(request);
        }
        public Model.GetTeeTimeResponse PutTeeTime(Model.PutTeeTimeRequest request)
        {
            return Invoke<Model.GetTeeTimeResponse>(request);
        }
        public Model.GetTeeTimeResponse UpdateTeeTime(Model.UpdateTeeTimeRequest request)
        {
            return Invoke<Model.GetTeeTimeResponse>(request);
        }
        public Model.BooleanResponse DeleteTeeTime(Model.DeleteTeeTimeRequest request)
        {
            return Invoke<Model.BooleanResponse>(request);
        }

        public Int32 GetTeeTimeCount()
        {
            var countString = Invoke<Model.StringResponse>(
                new Model.GetTeeTimeCountRequest()
                );

            // Make sure we have a response
            if (countString == null || 
                string.IsNullOrEmpty(countString.Data))
                return 0;

            Int32 count;
            if (Int32.TryParse(countString.Data, out count))
                return count;
            else
                return 0;
        }
        public Model.GetSimilarTeeTimeResponse GetSimilarTeeTimes(Model.GetSimilarTeeTimeRequest request)
        {
            // Who passes in an empty guid anyways?
            if (request.TeeTimeId < 1)
                throw new NullReferenceException("Invalid SearchId provided. Unsigned Long/Int64 expected.");

            return Invoke<Model.GetSimilarTeeTimeResponse>(request);
        }

        public Model.GetSearchResponse GetSearch(Model.GetSearchRequest request)
        {
            // Who passes in an empty guid anyways?
            if (request.SearchId < 1)
                throw new NullReferenceException("Invalid SearchId provided. Unsigned Long/Int64 expected.");

            return Invoke<Model.GetSearchResponse>(request);
        }
        public Model.GetSearchResponse PutSearch(Model.PutSearchRequest request)
        {
            return Invoke<Model.GetSearchResponse>(request);
        }
        public Model.GetSearchResponse UpdateSearch(Model.UpdateSearchRequest request)
        {
            return Invoke<Model.GetSearchResponse>(request);
        }
        public Model.BooleanResponse DeleteSearch(Model.DeleteSearchRequest request)
        {
            return Invoke<Model.BooleanResponse>(request);
        }

        public Model.GetRecentSearchResponse GetRecentSearches(Model.GetRecentSearchRequest request)
        {
            // Who passes in an empty guid anyways?
            if (string.IsNullOrEmpty(request.ExternalId))
                throw new NullReferenceException("ExternalId null or empty.");

            return Invoke<Model.GetRecentSearchResponse>(request);
        }

        public Model.GetInventoryResponse GetInventory(Model.PutSearchRequest request)
        {
            if (request == null || request.Search == null)
                throw new ArgumentNullException("request", "search null or empty");

            return Invoke<Model.GetInventoryResponse>(request, "/teetimes.xml");
        }
        public Model.GetInventoryResponse GetInventory(Model.GetInventoryRequest request)
        {
            // Who passes in an empty guid anyways?
            if (request.SearchId < 1)
                throw new NullReferenceException("Invalid SearchId provided. Unsigned Long/Int64 expected.");

            return Invoke<Model.GetInventoryResponse>(request);
        }
        

        public Model.BooleanResponse BatchPutTeeTimes(Model.BatchPutTeeTimesRequest request)
        {
            if (request == null || request.TeeTimes == null || request.TeeTimes.Count < 1)
                throw new ArgumentNullException("Request or TeeTime collection empty/null");

            if (request.TeeTimes.Count > _MaxBatchSize)
                throw new ArgumentOutOfRangeException("TeeTimes",
                    "You provided more than the max of _MaxBatchSize items. Please break your request into smaller chunks");

            return Invoke<Model.BooleanResponse>(request);
        }
        public Model.BooleanResponse BatchDeleteTeeTimes(Model.BatchDeleteTeeTimesRequest request)
        {
            if (request == null || request.TeeTimeIds == null || request.TeeTimeIds.Count < 1)
                throw new ArgumentNullException("Request or TeeTimeId collection empty or null");

            if (request.TeeTimeIds.Count > _MaxBatchSize)
                throw new ArgumentOutOfRangeException("TeeTimeIds",
                    "You provided more than the max of _MaxBatchSize items. Please break your request into smaller chunks");

            return Invoke<Model.BooleanResponse>(request);
        }


        public Model.GetReservationResponse GetReservation(Model.GetReservationRequest request)
        {
            // Who passes in an empty guid anyways?
            if (request.ReservationId < 1)
                throw new NullReferenceException("Invalid ReservationId provided. Unsigned Long/Int64 expected.");

            return Invoke<Model.GetReservationResponse>(request);
        }
        public Model.PutReservationResponse PutReservation(Model.PutReservationRequest request)
        {
            return Invoke<Model.PutReservationResponse>(request);
        }

        public Model.GetCourseAverageResponse GetCourseAverages()
        {
            return Invoke<Model.GetCourseAverageResponse>(
                new Model.GetCourseAverageRequest()
                );
        }

        #endregion

        #region Constructors

        public static InventoryClient Load()
        {
            return Create(null);
        }

        /// <summary>
        /// Load/Create client from the config
        /// </summary>
        /// <param name="configSectionName">Nullable - Defaults to golfsigma/inventory</param>
        /// <returns></returns>
        public static InventoryClient Create(string configSectionName)
        {
            // Get our config
            var config = Configuration.AuthenticationSection.Load(configSectionName);

            // Format the useragent if provided
            string userAgent = "";
            if (!string.IsNullOrEmpty(config.UserAgent))
            {
                var callingName = Assembly.GetCallingAssembly().GetName();
                var callingVersion = callingName.Version;
                userAgent = string.Format(
                    config.UserAgent,
                    callingName.Name,
                    callingName.FullName,
                    callingVersion.ToString(),
                    callingVersion.Major,
                    callingVersion.Minor,
                    callingVersion.Build,
                    callingVersion.Revision
                    );
            }

            // Get the max retries
            int maxRetries = 2;
            if (!string.IsNullOrEmpty(config.MaxErrorRetry))
                int.TryParse(config.MaxErrorRetry, out maxRetries);

            int requestTimeout = 30;
            if (!string.IsNullOrEmpty(config.RequestTimeout))
                int.TryParse(config.RequestTimeout, out requestTimeout);

            // Create the client
            var client = new InventoryClient(
                config.AccessKeyId,
                config.SecretAccessKey,
                new GolfSigma.Inventory.InventoryConfig()
                {
                    MaxErrorRetry = maxRetries,
                    ServiceURL = config.ServiceURL,
                    UserAgent = userAgent,
                    RequestTimeout = requestTimeout,
                    DisableCompression = config.DisableCompression
                });

            // Set the proxy
            if (!string.IsNullOrEmpty(config.ProxyHost) &&
                !string.IsNullOrEmpty(config.ProxyPort))
            {
                int proxyPort;
                if (!int.TryParse(config.ProxyPort, out proxyPort))
                    throw new ArgumentException("Invalid port, expecting integer", "proxyPort");

                client.Config.ProxyHost = config.ProxyHost;
                client.Config.ProxyPort = proxyPort;
            }

            // Return our client
            return client;
        }

        /// <summary>
        /// Constructs InventoryClient with GolfSigma Access Key ID and Secret Key
        /// </summary>
        /// <param name="awsAccessKeyId">AWS Access Key ID</param>
        /// <param name="awsSecretAccessKey">AWS Secret Access Key</param>
        public InventoryClient(String accessKeyId, String secretAccessKey) : this(accessKeyId, secretAccessKey, new InventoryConfig())
        { }

        /// <summary>
        /// Constructs InventoryClient with GolfSigma Access Key ID and Secret Key
        /// </summary>
        /// <param name="awsAccessKeyId">Access Key ID</param>
        /// <param name="awsSecretAccessKey">Secret Access Key</param>
        /// <param name="config">configuration</param>
        public InventoryClient(string accessKeyId, string secretAccessKey, InventoryConfig config)
        {
            this._AccessKeyId = accessKeyId;
            this._SecretAccessKey = secretAccessKey;
            this.Config = config;

            // Make sure expect 100 is disabled
            ServicePointManager.Expect100Continue = false;
        }

        #endregion

        #region Invoke

        /// <summary>
        /// Configure HttpClient with set of defaults as well as configuration
        /// from InventoryConfig instance. Defaults to "GET" verb.
        /// </summary>
        /// <param name="contentLength"></param>
        /// <returns></returns>
        private HttpWebRequest CreateWebRequest(Model.IRequest item, string overrideHttpPath)
        {
            string path = Config.ServiceURL + item.HttpPath;

            if (!string.IsNullOrEmpty(overrideHttpPath))
                path = Config.ServiceURL + overrideHttpPath;

            HttpWebRequest request = WebRequest.Create(path) as HttpWebRequest;

            if (Config.IsSetProxyHost())
                request.Proxy = new WebProxy(Config.ProxyHost, Config.ProxyPort);

            // Set some standard properties
            request.Method = item.HttpVerb.ToUpper();
            request.UserAgent = Config.UserAgent;
            request.Timeout = Config.RequestTimeout * 1000;
            request.Headers.Add(GetHeaderKey("timestamp"), GetHttpDate());
            request.KeepAlive = false;

            // Make sure expect 100 is disabled
            ServicePointManager.Expect100Continue = false;

            if (item.HttpHeaders != null && item.HttpHeaders.Count > 0)
            {
                foreach (KeyValuePair<string, string> headerItem in item.HttpHeaders)
                    request.Headers.Add(headerItem.Key, headerItem.Value);
            }

            // Mono does NOT support automatic decompression
            if (!Config.DisableCompression && !Config.IsMono)
                request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
            
            // Block auto redirect as ALL requests require authentication. 
            request.AllowAutoRedirect = false;

            // Return the webRequest
            return request;
        }

        private string GetHeaderKey(string key)
        {
            return string.Concat(
                _HeaderPrefix,
                key.Replace(" ", "-")
                ).ToLower();
        }

        private T Invoke<T>(Model.IRequest item)
        {
            return Invoke<T>(item, null);
        }

        /// <summary>
        /// Invoke request and return response
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="item"></param>
        /// <returns></returns>
        private T Invoke<T>(Model.IRequest item, string overrideHttpPath)
        {
            // Create our defaults
            T response = default(T);
            string responseBody = null;
            HttpStatusCode statusCode = default(HttpStatusCode);

            // Get our request body
            string requestBody = GetParametersAsString(item.HttpParameters);

            // Invoke the webservice
            bool shouldRetry = true;
            int retries = 0;
            do
            {
                // Create our webRequest
                HttpWebRequest request = CreateWebRequest(item, overrideHttpPath);

                // Authorize the request
                AuthorizeRequest(request, requestBody);

                // Submit the request and read response body
                try
                {
                    // Post our data
                    if (!string.IsNullOrEmpty(requestBody))
                    {
                        // Encode the body
                        var encodedBody = new UTF8Encoding().GetBytes(requestBody);

                        // Set the content type and length
                        request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
                        request.ContentLength = encodedBody.Length;

                        using (Stream requestStream = request.GetRequestStream())
                        {
                            requestStream.Write(encodedBody, 0, encodedBody.Length);
                        }
                    }

                    // Get our response
                    using (HttpWebResponse httpResponse = request.GetResponse() as HttpWebResponse)
                    {
                        // Get the status code
                        statusCode = httpResponse.StatusCode;

                        // Get our response stream
                        var responseStream = httpResponse.GetResponseStream();

                        // Determine if the response is compressed
                        if (httpResponse.ContentEncoding.ToLower().Contains("gzip"))
                            responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
                        else if (httpResponse.ContentEncoding.ToLower().Contains("deflate"))
                            responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);

                        using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
                        {
                            responseBody = reader.ReadToEnd();
                        }

                        responseStream.Close();
                        responseStream.Dispose();
                        httpResponse.Close();
                    }

                    // Attempt to deserialize response into <Action> Response type
                    if (!string.IsNullOrEmpty(responseBody))
                    {
                        XmlSerializer serlizer = new XmlSerializer(typeof(T));
                        response = (T)serlizer.Deserialize(new StringReader(responseBody));
                    }

                    // Must be successful DON'T retry
                    shouldRetry = false;
                }
                // Web exception is thrown on unsucessful responses
                catch (WebException we)
                {
                    // Make sure we DON'T retry
                    shouldRetry = false;

                    if (we.Message == "The underlying connection was closed: The connection was closed unexpectedly." ||
                        we.Message.Contains("Unable to connect to the remote server") ||
                        we.Message.Contains("500 Internal Server Error") ||
                        we.Message.Contains("Unhandled error has occurred")
                        )
                    {
                        shouldRetry = true;
                        PauseOnRetry(++retries, HttpStatusCode.InternalServerError);
                    }
                    else
                    {
                        // Get our response
                        using (HttpWebResponse httpErrorResponse = (HttpWebResponse)we.Response as HttpWebResponse)
                        {
                            // Get the status code
                            statusCode = httpErrorResponse.StatusCode;

                            // Get our response stream
                            var responseStream = httpErrorResponse.GetResponseStream();

                            // Determine if the response is compressed
                            if (httpErrorResponse.ContentEncoding.ToLower().Contains("gzip"))
                                responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
                            else if (httpErrorResponse.ContentEncoding.ToLower().Contains("deflate"))
                                responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);

                            using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
                            {
                                responseBody = reader.ReadToEnd();
                            }

                            responseStream.Close();
                            responseStream.Dispose();
                        }

                        // If it is an internal server error or service unavail, retry the request
                        if (statusCode == HttpStatusCode.InternalServerError || statusCode == HttpStatusCode.ServiceUnavailable)
                        {
                            shouldRetry = true;
                            PauseOnRetry(++retries, statusCode);
                        }
                        else
                        {
                            // Attempt to deserialize response into ErrorResponse type
                            try
                            {
                                XmlSerializer serlizer = new XmlSerializer(typeof(Model.Error));
                                Model.Error error = (Model.Error)serlizer.Deserialize(new StringReader(responseBody));

                                // Throw formatted exception with information available from the error response
                                throw new InventoryException(
                                    error.Message,
                                    statusCode,
                                    error.Code,
                                    error.Type,
                                    "UNKNOWN", // RequestId
                                    responseBody);
                            }
                            // Rethrow on deserializer error
                            catch (Exception e)
                            {
                                if (e is InventoryException)
                                    throw e;
                                else
                                {
                                    InventoryException se = ReportAnyErrors(responseBody, statusCode, e);
                                    throw se;
                                }
                            }
                        }
                    }
                }

                // Catch other exceptions, attempt to convert to formatted exception,
                // else rethrow wrapped exception
                catch (Exception e)
                {
                    throw new InventoryException(e);
                }
            } while (shouldRetry);

            // Return the response
            return response;
        }

        /// <summary>
        /// Exponential sleep on failed request
        /// </summary>
        /// <param name="retries"></param>
        /// <param name="status"></param>
        private void PauseOnRetry(int retries, HttpStatusCode status)
        {
            if (retries <= Config.MaxErrorRetry)
            {
                int delay = (int)Math.Pow(4, retries) * 100;
                System.Threading.Thread.Sleep(delay);
            }
            else
            {
                throw new InventoryException("Maximum number of retry attempts reached : " + (retries - 1), status);
            }
        }

        /// <summary>
        /// Look for additional error strings in the response and return formatted exception
        /// </summary>
        /// <param name="responseBody"></param>
        /// <param name="status"></param>
        /// <param name="e"></param>
        /// <returns></returns>
        private InventoryException ReportAnyErrors(string responseBody, HttpStatusCode status, Exception e)
        {
            InventoryException ex = null;

            if (responseBody != null && responseBody.StartsWith("<"))
            {
                Match errorMatcherOne = Regex.Match(responseBody, "<RequestId>(.*)</RequestId>.*<Error>" +
                        "<Code>(.*)</Code><Message>(.*)</Message></Error>.*(<Error>)?", RegexOptions.Multiline);
                Match errorMatcherTwo = Regex.Match(responseBody, "<Error><Code>(.*)</Code><Message>(.*)" +
                        "</Message></Error>.*(<Error>)?.*<RequestID>(.*)</RequestID>", RegexOptions.Multiline);
                Match errorMatcherThree = Regex.Match(responseBody, "<Error><Code>(.*)</Code><Message>(.*)" +
                        "</Message><BoxUsage>(.*)</BoxUsage></Error>.*(<Error>)?.*<RequestID>(.*)</RequestID>", RegexOptions.Multiline);

                if (errorMatcherOne.Success)
                {
                    string requestId = errorMatcherOne.Groups[1].Value;
                    string code = errorMatcherOne.Groups[2].Value;
                    string message = errorMatcherOne.Groups[3].Value;

                    ex = new InventoryException(message, status, code, "Unknown", requestId, responseBody);

                }
                else if (errorMatcherTwo.Success)
                {
                    string code = errorMatcherTwo.Groups[1].Value;
                    string message = errorMatcherTwo.Groups[2].Value;
                    string requestId = errorMatcherTwo.Groups[4].Value;

                    ex = new InventoryException(message, status, code, "Unknown", requestId, responseBody);
                }
                else if (errorMatcherThree.Success)
                {
                    string code = errorMatcherThree.Groups[1].Value;
                    string message = errorMatcherThree.Groups[2].Value;
                    string boxUsage = errorMatcherThree.Groups[3].Value;
                    string requestId = errorMatcherThree.Groups[5].Value;

                    ex = new InventoryException(message, status, code, "Unknown", requestId, responseBody);
                }
                else
                {
                    ex = new InventoryException("Internal Error", status);
                }
            }
            else
            {
                ex = new InventoryException("Internal Error", status);
            }
            return ex;
        }

        /// <summary>
        /// Convert Dictionary of paremeters to Url encoded query string
        /// </summary>
        /// <param name="parameters"></param>
        /// <returns></returns>
        private string GetParametersAsString(IDictionary<String, String> parameters)
        {
            // Pointless to continue if we don't have any params
            if (parameters == null || parameters.Count < 1)
                return null;

            StringBuilder data = new StringBuilder();
            foreach (String key in (IEnumerable<String>)parameters.Keys)
            {
                String value = parameters[key];
                if (value != null)
                {
                    data.Append(key);
                    data.Append('=');
                    data.Append(UrlEncode(value, false));
                    data.Append('&');
                }
            }
            String result = data.ToString();
            return result.Remove(result.Length - 1);
        }

        #endregion

        #region Signature

        /// <summary>
        /// Computes RFC 2104-compliant HMAC signature for request parameters
        /// Implements GolfSigma Signature, as per following spec:
        /// 
        /// If Signature Version is 1, string to sign is based on following:
        /// 
        ///  1. The HTTP Host header in the form of lowercase host, followed by an ASCII newline.
        ///  2. The URL encoded HTTP absolute path component of the URI
        ///     (up to but not including the query string parameters);
        ///     if this is empty use a forward '/'. This parameter is followed by an ASCII newline.
        ///  3. The concatenation of all query string components (names and values)
        ///     as UTF-8 characters which are URL encoded as per RFC 3986
        ///     (hex characters MUST be uppercase)
        ///     Parameter names are separated from their values by the '=' character
        ///     (ASCII character 61), even if the value is empty.
        ///     Pairs of parameter and values are separated by the '&' character (ASCII code 38).
        /// </summary>
        /// <param name="parameters"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private void AuthorizeRequest(HttpWebRequest request, string body)
        {
            // Make sure we have a valid version (only v1 is supported right now)
            if (Config.SignatureVersion != "1")
                throw new Exception("Invalid Signature Version specified");

            // Get the hasing algorith
            KeyedHashAlgorithm algorithm = KeyedHashAlgorithm.Create(Config.SignatureMethod.ToUpper());

            // Build the string to sign
            StringBuilder stringToSign = new StringBuilder();

            // Append the method / verb
            stringToSign.Append(request.Method.ToUpper());
            stringToSign.Append("\n");

            // Append the host header
            stringToSign.Append(request.RequestUri.Authority.ToLower());
            stringToSign.Append("\n");

            // Append the path
            stringToSign.Append(request.RequestUri.PathAndQuery.ToLower());
            stringToSign.Append("\n");

            // Append the timestamp
            stringToSign.Append(request.Headers[GetHeaderKey("timestamp")]);
            stringToSign.Append("\n");

            // Append the body (if we have one)
            if (!string.IsNullOrEmpty(body))
                stringToSign.Append(body);

            // Generate the authorization value
            string signedRequest = Sign(stringToSign.ToString(), algorithm);

            // Attach the authorization
            request.Headers.Add(
                HttpRequestHeader.Authorization,
                string.Format(
                    "{0}|{1}",
                    _AccessKeyId,
                    signedRequest)
                );

            algorithm.Clear();
        }

        /// <summary>
        /// URL encode a string. Little different than the built in .NET method (ex. uses %20 vs. + as a space)
        /// </summary>
        /// <param name="data"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        private string UrlEncode(string data, bool path)
        {
            StringBuilder encoded = new StringBuilder();
            String unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~" + (path ? "/" : "");

            foreach (char symbol in System.Text.Encoding.UTF8.GetBytes(data))
            {
                if (unreservedChars.IndexOf(symbol) != -1)
                {
                    encoded.Append(symbol);
                }
                else
                {
                    encoded.Append("%" + String.Format("{0:X2}", (int)symbol));
                }
            }

            return encoded.ToString();

        }

        /// <summary>
        /// Computes RFC 2104-compliant HMAC signature.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        /// <param name="algorithm"></param>
        /// <returns></returns>
        private string Sign(string data, KeyedHashAlgorithm algorithm)
        {
            Encoding encoding = new UTF8Encoding();
            algorithm.Key = encoding.GetBytes(_SecretAccessKey);
            return Convert.ToBase64String(
                algorithm.ComputeHash(
                    encoding.GetBytes(
                        data.ToCharArray()
                        )
                    )
                );
        }

        /// <summary>
        /// Formats date as the HTTP standard
        /// </summary>
        /// <returns></returns>
        private string GetHttpDate()
        {
            return DateTime.UtcNow.ToString(
                "ddd, dd MMM yyyy HH:mm:ss ",
                System.Globalization.CultureInfo.InvariantCulture) + "GMT";
        }

        #endregion

        #region Forwarded For

        private void SetForwardedFor(ref HttpWebRequest request)
        {
            if (Config.SetForwardedFor &&
                System.Web.HttpContext.Current != null &&
                System.Web.HttpContext.Current.Request != null &&
                System.Web.HttpContext.Current.Request.Headers != null)
            {
                var webRequestHeaders = System.Web.HttpContext.Current.Request.Headers;
                var xForwardedFor = webRequestHeaders.Get("x-forwarded-for");
                var UserHostAddress = System.Web.HttpContext.Current.Request.UserHostAddress;

                // Set the value
                if (string.IsNullOrEmpty(xForwardedFor))
                    xForwardedFor = UserHostAddress;
                else
                    xForwardedFor = string.Concat(
                        UserHostAddress,
                        ", ",
                        xForwardedFor
                        );

                // Set the header
                request.Headers.Add("x-forwarded-for", xForwardedFor);
            }
        }

        #endregion Forwarded For
    }
}
